﻿namespace StockSharp.AlfaDirect
{
	using System;
	using System.Collections.Generic;
	using System.Diagnostics;
	using System.Globalization;
	using System.Linq;
	using System.Threading;

	using Ecng.Collections;
	using Ecng.Common;
	using Ecng.ComponentModel;
	using Ecng.Interop;

	using StockSharp.BusinessEntities;
	using StockSharp.Algo;
	using StockSharp.Algo.Candles;
	using StockSharp.Logging;
	 
	internal struct CandleTimeInfo
	{
		internal DateTime TimeStarted;
		internal bool IsStartedSent;
	};

	/// <summary>
	/// Реализация интерфейса <see cref="ITrader"/>, предоставляющая шлюз взаимодействия с системой AlfaDirect.
	/// </summary>
	public sealed class AlfaTrader : BaseTrader, IExternalCandleSource
	{
		private volatile AlfaWrapper _wrapper;
		private readonly object _wrapperCreationMutex = new object();
		private Timer _candlesTimer;
		//private DateTime _creationTime;
		private readonly Stopwatch _elapsedTime = new Stopwatch();
		private readonly SynchronizedOrderedList<int> _tradeSubscribers = new SynchronizedOrderedList<int>();
		private readonly SynchronizedOrderedList<int> _securitySubscribers = new SynchronizedOrderedList<int>();
		private readonly SynchronizedList<Order> _pendingCancellationOrders = new SynchronizedList<Order>();
		private readonly SynchronizedList<Order> _pendingCreationOrders = new SynchronizedList<Order>(); 

		private readonly SynchronizedDictionary<CandleSeries, CandleTimeInfo> _series = new SynchronizedDictionary<CandleSeries, CandleTimeInfo>();

		/// <summary>
		/// Конструктор.
		/// </summary>
		public AlfaTrader()
			: base(Platforms.x86)
		{
			TransactionIdGenerator = new RngCryptoTransactionIdGenerator();
			IsSupportAtomicReRegister = false;
		}

		/// <summary>
		/// Имя пользователя в терминале Альфа-Директ
		/// </summary>
		public string Login { set; get; }

		/// <summary>
		/// Пароль для входа в терминал
		/// </summary>
		public string Password { set; private get; }

		/// <summary>
		/// Отложенная инициализация AlfaWrapper
		/// </summary>
		private AlfaWrapper Wrapper
		{
			get
			{
				if (_wrapper == null)
				{
					lock (_wrapperCreationMutex)
					{
						if (_wrapper == null)
						{
							var wrapper = new AlfaWrapper();

							wrapper.ProcessMarketTime += time =>
							{
								//_creationTime = time;
								_elapsedTime.Start();
							};

							wrapper.GetMarketTime();

							wrapper.ProcessSecurities += OnProcessSecurities;
							wrapper.ProcessOrders += OnProcessOrders;
							wrapper.ProcessPortfolios += OnProcessPortfolios;
							wrapper.ProcessQuotes += OnProcessQuotes;
							wrapper.ProcessTrades += OnProcessTrades;
							wrapper.ProcessPositions += OnProcessPositions;
							wrapper.ProcessMyTrades += OnProcessMyTrades;
							wrapper.ConnectionError += OnConnectionError;
							wrapper.Connected += OnWrapperConnected;
							wrapper.Disconnected += OnWrapperDisconnected;

							//this.AddInfoLog("AlfaDirect v. {0}", wrapper.Version);

							_wrapper = wrapper;
						}
					}
				}

				return _wrapper;
			}
		}

		/// <summary>
		/// Текстовое описание шлюза.
		/// </summary>
		public override string DisplayName
		{
			get { return Login; }
		}

		private void OnWrapperDisconnected()
		{
			this.AddInfoLog("Соединение с Альфа-Директ разорвано");
			IsConnected = false;
			RaiseDisconnected();
		}

		private void OnWrapperConnected()
		{
			this.AddInfoLog("Соединение с Альфа-Директ установлено");
			IsConnected = true;
			RaiseConnected();
		}

		private void OnConnectionError(Exception ex)
		{
			this.AddInfoLog("Ошибка подключения: {0}".Put(ex.Message));
			RaiseConnectionError(ex);
		}

		private void OnWrapperNewDataExported()
		{
			RaiseNewDataExported();
		}

		private void OnProcessCandles()
		{
			if (!IsConnected || !IsExportStarted)
			{
				this.AddWarningLog("Failed to process candles");
				return;	
			}

			_series.SyncDo(dictionary =>
			{
				var keys = new List<CandleSeries>(dictionary.Keys);

				foreach (var series in keys)
				{
					var timeInfo = dictionary[series];
					var timeFrame = (AlfaTimeFrames)series.Arg;

					var finishTime = series.GetMarketTime() + (TimeSpan)timeFrame;

					if(finishTime < timeInfo.TimeStarted)
					{
						finishTime = timeInfo.TimeStarted + (TimeSpan)timeFrame;	
					}
					ProcessHistoryCandles(series, timeFrame, new Range<DateTime>(timeInfo.TimeStarted, finishTime), false, ref timeInfo);

					dictionary[series] = timeInfo;
				}
			});
		}

		private void OnProcessTrades(string data)
		{
			var result = AlfaUtils.Filter(data, _tradeSubscribers);

			foreach (var pair in result)
			{
				var details = pair.Value;
				ProcessSecurityAction(null, pair.Key, security => ProcessEvents(() =>
				{
					foreach (var d in details)
					{
						var tradeDetails = d;

						GetTrade(security, tradeDetails[1].To<long>(), id =>
						{
							var trade = EntityFactory.CreateTrade(security, id);

							trade.OrderDirection = AlfaUtils.OrderDirectionsFromAlfa(tradeDetails[5].To<int>());
							trade.Price = tradeDetails[3].To<decimal>();
							trade.Time = tradeDetails[4].To<DateTime>();
							trade.Volume = tradeDetails[2].To<decimal>();

							security.LastTrade = trade;

							security.RaisePropertyChanged();
							RaiseSecuritiesChanged(new[] { security });

							return trade;
						});
					}
				}));
			}
		}

		private void OnProcessQuotes(string tableParams, string data)
		{
			// tableParams: paper_no = 36955
			var paperNo = tableParams.Split(new[] { '=' })[1].Trim().To<int>();

			ProcessSecurityAction(null, paperNo, security =>
			{
				//this.AddInfoLog("OnProcessQuotes(): {0}", security.Id);

				var quotes = data.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);

				ProcessEvents(() =>
				{
					var bids = new List<Quote>();
					var asks = new List<Quote>();

					foreach (var quoteStr in quotes)
					{
						// quoteStr: 0|10138|14|

						var details = quoteStr.Split('|');
						var sellQty = details[0].To<decimal>();
						var price = details[1].To<decimal>();
						var buyQty = details[2].To<decimal>();

						if(sellQty == 0 && buyQty == 0)
						{
							// If the sellQty and buyQty are 0 - that is our limit order 
							// which is not a part of the market depth. Just skip it.
							continue;
						}

						var quote = new Quote
						{
							Price = price,
							Security = security
						};

						if (sellQty == 0)
						{
							quote.OrderDirection = OrderDirections.Buy;
							quote.Volume = buyQty;

							bids.Insert(0, quote);
						}
						else if (buyQty == 0)
						{
							quote.OrderDirection = OrderDirections.Sell;
							quote.Volume = sellQty;

							asks.Add(quote);
						}
					}

					RaiseMarketDepthChanged(GetMarketDepth(security).Update(bids, asks, true, security.GetMarketTime()));
				});
			}, true);
		}

		private void OnProcessPortfolios(string data)
		{
			//this.AddInfoLog("OnProcessPortfolios() {0}", data);

			if(data.IsNull())
			{
				return;
			}

			var portfolios = data.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);

			ProcessEvents(() =>
			{
				foreach (var str in portfolios)
				{
					var details = str.Split('|');

					if(details[1] != "money")
					{
						continue;
					}

					var exchangeBoard = AlfaUtils.ExchangeBoardCodeToExchangeBoard(details[2]);
					var portfolioName = "{0}@{1}".Put(details[0], details[2]);

					GetPortfolio(portfolioName,
					name =>
					{
						var portfolio = EntityFactory.CreatePortfolio(name);

						portfolio.ExchangeBoard = exchangeBoard;
						portfolio.BeginValue = details[3].To<decimal>();
						portfolio.VariationMargin = GetCurrentAmount(details[0], details[2], portfolios);
						portfolio.CurrentValue = portfolio.BeginValue + portfolio.VariationMargin;
						return portfolio;
					},
					portfolio =>
					{
						portfolio.BeginValue = details[3].To<decimal>();
						portfolio.VariationMargin = GetCurrentAmount(details[0], details[2], portfolios);
						portfolio.CurrentValue = portfolio.BeginValue + portfolio.VariationMargin;
						return true;
					});
				}
			});
		}

		private static decimal GetCurrentAmount(string portfolio, string exchange, IEnumerable<string> data)
		{
			var amount = 0m;

			foreach (var str in data)
			{
				var details = str.Split('|');

				if (details[1] == "money")
				{
					continue;
				}

				if(details[0] == portfolio && details[2]== exchange)
				{
					amount += details[4].To<decimal>();
				}
			}

			return amount;
		}

		private void OnProcessOrders(string data)
		{
			this.AddInfoLog("OnProcessOrders() {0}", data);

			if (data.IsNull())
			{
				return;
			}

			var ordersStr = data.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);

			ProcessEvents(() =>
			{
				foreach (var str in ordersStr)
				{
					var details = str.Split('|');

					// 83487901|41469-000|42550|M|B|8356|1|1|0:00:00||FORTS|0|
					// ...
					// 83487901|41469-000|42550|M|B|8356|1|0|12.12.2011 16:31:41|2198931532|FORTS|0|
					// NOTE: когда в первый раз приходит апдейт по ордеру в нем нет ни времени, ни комментария.

					// 84352688|41469-000|12910|M|S|80.13|10|0|22.12.2011 17:16:58||MICEX_SHR|0|0|
					// 84352688|41469-000|12910|M|S|80.13|10|0|22.12.2011 17:16:58|2968835969|MICEX_SHR|0|80.408|
					// NOTE: на ММВБ ордер сперва приходит с пустым комментарием!

//					var timeStr = details[8];

//					if (timeStr == "0:00:00") //  || details[9].Length == 0
//					{
//						continue;
//					}

					//var transactionId = details[9].Length > 0 ? details[9].To<long>() : TransactionIdGenerator.GetNextId();
					//var transactionId = details[9].To<long>();

					//var transactionId = trStr.Length > 0 ? details[9].To<long>() : TransactionIdGenerator.GetNextId();

					ProcessSecurityAction(null, details[2].To<int>(), security =>
						GetOrder(security, details[0].To<long>(), id =>
						{
							Order order = null;

							long transactionId;

							var pendingOrder = _pendingCreationOrders.FirstOrDefault(o => o.Id == id);
							
							if(pendingOrder != null)
							{
								transactionId = pendingOrder.TransactionId;
								_pendingCreationOrders.Remove(pendingOrder);

								Trace.WriteLine("Creating order from pending: id {0}, tr.id {1}".Put(pendingOrder.Id, pendingOrder.TransactionId));

								order = pendingOrder;
							}
							else
							{
								var trStr = details[9];
								transactionId = trStr.Length > 0 ? trStr.To<long>() : TransactionIdGenerator.GetNextId();
							}

							this.AddInfoLog("create order: id = {0}, tr. id = {1}", id, transactionId);

							if(order == null)
							{
								var stopPrice = details[11].To<decimal>();
								order = (stopPrice != 0) ? GetStopOrderByTransactionId(transactionId) : GetOrderByTransactionId(transactionId);

								if (order == null)
								{
									this.AddInfoLog("Создание ордера из терминала, tr. id {0}", transactionId);
									order = EntityFactory.CreateOrder(security, id);
									order.TransactionId = transactionId;

									order.Trader = this;
									//order.Id = details[0].To<long>();

									order.Portfolio = GetPortfolio("{0}@{1}".Put(details[1], details[10]));
									order.Security = security;
									order.Direction = AlfaUtils.OrderDirectionsFromAlfa(details[4]);

									order.Type = OrderTypes.Limit;

									// TODO: find limit/ limit + target / trailing orders
									if (stopPrice != 0)
									{
										order.Type = OrderTypes.Conditional;
										order.Condition = new AlfaLimitOrderCondition();
									}
								}
							}
							
							order.RaisePropertyChanged();

							return order;
						},
						order =>
						{
							using (order.BeginUpdate())
							{
								//order.Id = details[0].To<long>();
								order.Price = details[5].To<decimal>();
								order.Volume = details[6].To<Int32>();
								order.Balance = details[7].To<Int32>();
								order.Time = DateTime.Parse(details[8]);
								order.SetAlfaAveragePrice(details[12].To<decimal>());

								this.AddInfoLog("update order: id = {0}", order.Id);

								switch (details[3]) // статус
								{
									case "N": // принята
									case "O": // активная
									case "G": // с условием
									{
										order.State = OrderStates.Active;
										break;
									}
									case "M": // исполнена
									{
										if (order.Balance != 0)
										{
											order.State = OrderStates.Active;
										}
										else if (order.Balance != 0 || order.GetAlfaAveragePrice() != 0)
										{
											order.State = OrderStates.Done;
										}
										break;
									}
									case "W": // удалена
									{
										order.State = OrderStates.Done;
										order.LastChangeTime = security.GetMarketTime();
										break;
									}
									default:
									{
										this.AddInfoLog("Order status {0} is not taken into account", details[3]);
										break;
									}
								}

								order.RaisePropertyChanged();
							}

							RaiseOrderChanged(order);

							if (_pendingCancellationOrders.Contains(order))
							{
								this.AddWarningLog("Отмена после регистрации, id {0}, tr id {1}", order.Id, order.TransactionId);

								_pendingCancellationOrders.Remove(order);
								CancelOrder(order);
							}

							return true;
						}));
				}
			});
		}

		/// <summary>
		/// Creation/Update of the securities.
		/// </summary>
		/// <param name="data">AD raw data</param>
		private void OnProcessSecurities(string data)
		{
			var result = AlfaUtils.Filter(data, _securitySubscribers);

			foreach (var p in result)
			{
				var pair = p; 

				ProcessEvents(() =>
				{
					foreach (var d in pair.Value)
					{
						var details = d;

						if (details[1].IsEmpty())
						{
							continue;
						}

						var id = details[4] + "@" + details[5];

						GetSecurity(id, security =>
						{
							//this.AddInfoLog("Security update {0}.", security.Id);

							using (security.BeginUpdate())
							{
								security.ExtensionInfo = new Dictionary<object, object>();

								security.Name = details[1];
								security.ShortName = details[1];
								security.ExpiryDate = DateTime.Parse(details[2]);
								security.Code = details[4];
								security.State = AlfaUtils.SecurityStateFromAlfa(details[3]);

								// TODO: this must be done the in the AD task.
								var exCode = Wrapper.GetExchangeCode(details[5]);

								security.ExchangeBoard = AlfaUtils.ExchangeBoardCodeToExchangeBoard(exCode);
								security.MinLotSize = details[19].To<int>();

								security.SetPaperNo(details[0].To<int>());
								security.SetCurrency(details[6]);
								security.SetPlaceCode(details[5]);

								decimal priceStep, priceStepCost;

								// TODO: this must be done the in the AD task.
								Wrapper.GetPriceStepInfo(security, out priceStep, out priceStepCost);

								security.MinStepSize = priceStep;
								security.MinStepPrice = priceStepCost == 0 ? priceStep : priceStepCost;

								security.Type = AlfaUtils.BoardCodeToSecurityType(details[9]);

								// http://stocksharp.com/forum/yaf_postsm21467_Optsiony-v-Al-fa-Diriekt.aspx#post21467
								if (security.Type == SecurityTypes.Option)
								{
									var sec = security.Code.GetOptionInfo();

									security.OptionType = sec.OptionType;
									security.Strike = sec.Strike;
									security.UnderlyingSecurityId = sec.UnderlyingSecurityId;
									security.ImpliedVolatility = details[20].To<decimal>();
									security.TheorPrice = details[21].To<decimal>();
								}

								security.BestAsk = new Quote
								{
									Price = details[13].To<decimal>(),
									Volume = details[14].To<decimal>(),
									Security = security,
									OrderDirection = OrderDirections.Sell
								};

								security.BestBid = new Quote
								{
									Price = details[15].To<decimal>(),
									Volume = details[16].To<decimal>(),
									Security = security,
									OrderDirection = OrderDirections.Buy
								};

								security.LowPrice = details[17].To<decimal>();
								security.HighPrice = details[18].To<decimal>();
								security.OpenInterest = details[10].To<decimal>();
								security.OpenPrice = details[11].To<decimal>();
								security.ClosePrice = details[12].To<decimal>();

								security.MarginBuy = details[7].To<decimal>();
								security.MarginSell = details[8].To<decimal>();

								security.LastChangeTime = security.GetMarketTime();

								AddNativeSecurityId(security, details[0].To<int>());
							}

							return true;
						});
					}
				});
			}
		}

		/// <summary>
		/// Update positions
		/// </summary>
		private void OnProcessPositions(string data)
		{
			//this.AddInfoLog("OnProcessPositions() {0}", data);
			Trace.WriteLine("OnProcessPositions() {0}".Put(data));

			if (data.IsNull())
			{
				return;
			}

			var positionsStr = data.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);

			ProcessEvents(() =>
			{
				foreach (var str in positionsStr)
				{
					var details = str.Split('|');
					
					var portfolioName = "{0}@{1}".Put(details[0], details[2]);

					if(details[1] == "money")
					{
						GetPortfolio(portfolioName,
						portfolio =>
						{
							portfolio.BeginValue = details[3].To<decimal>();
							portfolio.VariationMargin = GetCurrentAmount(details[0], details[2], positionsStr);
							portfolio.CurrentValue = portfolio.BeginValue + portfolio.VariationMargin;
							return true;
						});

						continue;
					}

					var portf = GetPortfolio(portfolioName);
					var securityId = "{0}@{1}".Put(details[1], details[2]);

					ProcessSecurityAction(securityId, null, secur =>
						UpdatePosition(portf, secur, position =>
						{
							using (position.BeginUpdate())
						{
							// текущий размер позиции - сколько реально бумаг в позиции
							position.CurrentValue = details[3].To<long>();
							// при установке стопа с TargetProfit значение netto_rest уменьшается на размер стопа.
							// считаем это уменьшение = BlockedValue. 
							// тогда netto_rest = CurrentValue - BlockedValue, откуда получаем формулу
							position.BlockedValue = position.CurrentValue - details[6].To<long>();
							}
							return true;
						}));
				}
			});
		}

		/// <summary>
		/// Update my trades
		/// </summary>
		/// <param name="data"></param>
		private void OnProcessMyTrades(string data)
		{
			//this.AddInfoLog("OnProcessMyTrades() {0}", data);
			Trace.WriteLine("OnProcessMyTrades() {0}".Put(data));

			if (data.IsNull())
			{
				return;
			}

			var mytradesStr = data.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);

			ProcessEvents(() =>
			{
				foreach (var str in mytradesStr)
				{
					var details = str.Split('|');

					var tradeId = details[0].To<long>();
					var orderId = details[1].To<long>();

					if (orderId == 0)
					{
						this.AddWarningLog("Сделка с номером {0} имеет нулевой номер иниц. заявки", tradeId);
						continue;
					}

					ProcessSecurityAction(null, details[3].To<int>(), security =>
						AddMyTrade(security, orderId, 0, tradeId, id =>
						{
							var trade = EntityFactory.CreateTrade(security, id);

							trade.Price = details[4].To<decimal>();
							trade.Time = details[7].To<DateTime>();
							trade.Volume = details[5].To<decimal>();
							trade.OrderDirection = AlfaUtils.OrderDirectionsFromAlfa(details[6]);

							return trade;
						}, t => { }));
				}
			});
		}

		/// <summary>
		/// Зарегистрировать заявку на бирже.
		/// </summary>
		/// <param name="order">Заявка, содержащая информацию для регистрации.</param>
		protected override void OnRegisterOrder(Order order)
		{
			if (order == null)
			{
				throw new ArgumentNullException("order");
			}

			this.AddInfoLog("Регистрация ордера tr.id = {0}", order.TransactionId);

			order.Comment = order.TransactionId.ToString(CultureInfo.InvariantCulture);

			_pendingCreationOrders.Add(order);

			Wrapper.RegisterOrder(order);
		}

		/// <summary>
		/// Отменить заявку на бирже.
		/// </summary>
		/// <param name="order">Заявка, которую нужно отменять.</param>
		protected override void OnCancelOrder(Order order)
		{
			if (order == null)
				throw new ArgumentNullException("order");

			this.AddInfoLog("Canceling order: id = {0}, tr. id = {1}", order.Id, order.TransactionId);

			long id;

			using (order.BeginRead())
			{
				if (order.Balance == 0)
				{
					this.AddWarningLog("Balance of the order (tr.id {0}) to cancel is 0", order.TransactionId);
				}

				id = order.Id;
			}

			if (id == 0)
			{
				this.AddInfoLog("Cancel order. Id: {0}, added to pending cancellation", id);
				_pendingCancellationOrders.Add(order);
			}
			else
			{
				this.AddInfoLog("Canceling order. Id: {0}", id);
				Wrapper.CancelOrder(order);
			}
		}

		/// <summary>
		/// Отменить группу заявок на бирже по фильтру.
		/// </summary>
		/// <param name="isStopOrder">True, если нужно отменить только стоп-заявки, false - если только обычный и null - если оба типа.</param>
		/// <param name="portfolio">Портфель. Если значение равно null, то портфель не попадает в фильтр снятия заявок.</param>
		/// <param name="direction">Направление заявки. Если значение равно null, то направление не попадает в фильтр снятия заявок.</param>
		/// <param name="classCode">Код класса. Если переданная строка пустая, то код не попадает в фильтр снятия заявок.</param>
		/// <param name="security">Инструмент. Если значение равно null, то инструмент не попадает в фильтр снятия заявок.</param>
		public override void CancelOrders(bool? isStopOrder = null, Portfolio portfolio = null, OrderDirections? direction = null, string classCode = null, Security security = null)
		{
			Wrapper.CancelOrders(isStopOrder, portfolio, direction, classCode, security);
		}

		/// <summary>
		/// Подключиться к торговой системе.
		/// </summary>
		protected override void OnConnect()
		{
			this.AddInfoLog("OnConnect()");

			Wrapper.Connect(Login, Password);
		}

		/// <summary>
		/// Отключиться от торговой системы.
		/// </summary>
		protected override void OnDisconnect()
		{
			this.AddInfoLog("OnDisconnect()");

			IsConnected = false;

			RaiseDisconnected();
		}

		/// <summary>
		/// Начать получать новую информацию по портфелю.
		/// </summary>
		/// <param name="portfolio">Портфель, по которому необходимо начать получать новую информацию.</param>
		protected override void OnRegisterPortfolio(Portfolio portfolio)
		{
			base.OnRegisterPortfolio(portfolio);

			this.AddInfoLog("RegisterPortfolio: {0}", portfolio.Name);

			Wrapper.RegisterPortfolio(portfolio);
		}

		/// <summary>
		/// Остановить получение новой информации по портфелю.
		/// </summary>
		/// <param name="portfolio">Портфель, по которому необходимо остановить получение новой информации.</param>
		protected override void OnUnRegisterPortfolio(Portfolio portfolio)
		{
			base.OnUnRegisterPortfolio(portfolio);

			this.AddInfoLog("UnRegisterPortfolio: {0}", portfolio.Name);

			Wrapper.UnRegisterPortfolio(portfolio);
		}

		/// <summary>
		/// Начать получать новую информацию (например, <see cref="P:StockSharp.BusinessEntities.Security.LastTrade"/> или <see cref="P:StockSharp.BusinessEntities.Security.BestBid"/>) по инструменту.
		/// </summary>
		/// <param name="security">Инструмент, по которому необходимо начать получать новую информацию.</param>
		protected override void OnRegisterSecurity(Security security)
		{
			base.OnRegisterSecurity(security);

			this.AddInfoLog("RegisterSecurity: {0}", security.Id);

			if (_securitySubscribers.IsEmpty())
			{
				Wrapper.RegisterSecurity();
			}

			_securitySubscribers.Add(security.GetPaperNo());
		}

		/// <summary>
		/// Остановить получение новой информации.
		/// </summary>
		/// <param name="security">Инструмент, по которому необходимо остановить получение новой информации.</param>
		protected override void OnUnRegisterSecurity(Security security)
		{
			base.OnUnRegisterSecurity(security);

			this.AddInfoLog("UnRegisterSecurity: {0}", security.Id);

			_securitySubscribers.Remove(security.GetPaperNo());

			if (_securitySubscribers.IsEmpty())
			{
				Wrapper.UnRegisterSecurity();
			}
		}

		/// <summary>
		/// Начать получать котировки (стакан) по инструменту.
		/// Значение котировок можно получить через метод <see cref="M:StockSharp.BusinessEntities.ITrader.GetMarketDepth(StockSharp.BusinessEntities.Security)"/>.
		/// </summary>
		/// <param name="security">Инструмент, по которому необходимо начать получать котировки.</param>
		protected override void OnRegisterMarketDepth(Security security)
		{
			base.OnRegisterMarketDepth(security);

			this.AddInfoLog("RegisterQuotes: {0}", security.Id);

			Wrapper.RegisterQuotes(security);
		}

		/// <summary>
		/// Остановить получение котировок по инструменту.
		/// </summary>
		/// <param name="security">Инструмент, по которому необходимо остановить получение котировок.</param>
		protected override void OnUnRegisterMarketDepth(Security security)
		{
			base.OnUnRegisterMarketDepth(security);

			this.AddInfoLog("UnRegisterQuotes: {0}", security.Id);

			Wrapper.UnRegisterQuotes(security);
		}

		/// <summary>
		/// Начать получать сделки (тиковые данные) по инструменту.
		/// Новые сделки будут приходить через событие <see cref="E:StockSharp.BusinessEntities.ITrader.NewTrades"/>.
		/// </summary>
		/// <param name="security">Инструмент, по которому необходимо начать получать сделки.</param>
		protected override void OnRegisterTrades(Security security)
		{
			base.OnRegisterTrades(security);

			this.AddInfoLog("RegisterTrades: {0}", security.Id);

			if(_tradeSubscribers.IsEmpty())
			{
				Wrapper.RegisterTrades();	
			}

			_tradeSubscribers.Add(security.GetPaperNo());
		}

		/// <summary>
		/// Остановить получение сделок (тиковые данные) по инструменту.
		/// </summary>
		/// <param name="security">Инструмент, по которому необходимо остановить получение сделок.</param>
		protected override void OnUnRegisterTrades(Security security)
		{
			base.OnUnRegisterTrades(security);

			this.AddInfoLog("UnRegisterTrades: {0}", security.Id);

			_tradeSubscribers.Remove(security.GetPaperNo());

			if(_tradeSubscribers.IsEmpty())
			{
				Wrapper.UnRegisterTrades();	
			}
		}

		/// <summary>
		/// Запустить экспорт данных из торговой системы в программу (получение портфелей, инструментов, заявок и т.д.).
		/// </summary>
		public override void StartExport()
		{
			this.AddInfoLog("StartExport()");

			if (IsExportStarted)
			{
				this.AddWarningLog("Export is already started");
				return;
			}

			base.StartExport();

			Wrapper.StartExport();

			_candlesTimer = ThreadingHelper.Timer(OnProcessCandles).Interval(TimeSpan.FromSeconds(5));
		}

		/// <summary>
		/// Остановить экспорт данных из торговой системы в программу, запущенный через <see cref="M:StockSharp.BusinessEntities.ITrader.StartExport"/>.
		/// </summary>
		public override void StopExport()
		{
			this.AddInfoLog("StopExport()");

			if (!IsExportStarted)
			{
				this.AddWarningLog("Export is already stoped");
				return;
			}

			base.StopExport();

			if(!_tradeSubscribers.IsEmpty())
			{
				Wrapper.UnRegisterTrades();
			}

			if (_tradeSubscribers.IsEmpty())
			{
				Wrapper.UnRegisterSecurity();
			}

			Wrapper.StopExport();

			// TODO: move these methods to StopExport.
			Wrapper.StopExportOrders();
			Wrapper.StopExportPositions();
			Wrapper.StopExportMyTrades();

			_candlesTimer.Dispose();
		}

		/// <summary>
		/// Получить исторические данные (тайм-фрейм свечки) от сервера Альфа-Директ.
		/// </summary>
		/// <param name="security">Инструмент, для которого необходимо получить исторические данные.</param>
		/// <param name="timeFrame">Тайм-фрейм.</param>
		/// <param name="range">Диапазон времени, для которого нужно получить данные.</param>
		/// <returns>Исторические данные.</returns>
		public IEnumerable<Candle> GetHistoryData(Security security, AlfaTimeFrames timeFrame, Range<DateTime> range)
		{
			if (security == null)
				throw new ArgumentNullException("security");

			if (timeFrame == null)
				throw new ArgumentNullException("timeFrame");

			if (range == null)
				throw new ArgumentNullException("range");

			this.AddInfoLog("GetHistoryData: security = {0}", security.Id);

			var data = _wrapper.GetHistoryData(security, timeFrame, range);

			var result = new List<Candle>();

			if(data != null)
			{
				this.AddInfoLog(data);

				var candlesStr = data.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);

				foreach (var str in candlesStr)
				{
					var details = str.Split('|');

					var candle = new TimeFrameCandle
					{
						Security = security,
						OpenPrice = details[1].To<decimal>(),
						HighPrice = details[2].To<decimal>(),
						LowPrice = details[3].To<decimal>(),
						ClosePrice = details[4].To<decimal>(),
						TotalVolume = details[5].To<decimal>(),
						TimeFrame = (TimeSpan)timeFrame,
						// из терминала приходит время закрытия свечи
						OpenTime = details[0].To<DateTime>() - (TimeSpan)timeFrame,
						CloseTime = details[0].To<DateTime>(),
					};

					result.Add(candle);
				}				
			}

			return result;
		}

		/// <summary>
		/// Получить исторические данные (тайм-фрейм свечки) из локальной базы Альфа-Директ.
		/// </summary>
		/// <param name="security">Инструмент, для которого необходимо получить исторические данные.</param>
		/// <param name="timeFrame">Тайм-фрейм.</param>
		/// <param name="range">Диапазон времени, для которого нужно получить данные.</param>
		/// <returns>Исторические данные.</returns>
		public IEnumerable<Candle> GetLocalHistoryData(Security security, AlfaTimeFrames timeFrame, Range<DateTime> range)
		{
			if (security == null)
				throw new ArgumentNullException("security");

			if (timeFrame == null)
				throw new ArgumentNullException("timeFrame");

			if (range == null)
				throw new ArgumentNullException("range");

			//this.AddInfoLog("GetLocalHistoryData: {0}, {1} - {2}", security.Id, range.Min, range.Max);

			var data = _wrapper.GetLocalHistoryData(security, timeFrame, range);

			var result = new List<Candle>();

			if (data != null)
			{
				//this.AddInfoLog(data);

				var candlesStr = data.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);

				foreach (var str in candlesStr)
				{
					var details = str.Split('|');

					var candle = new TimeFrameCandle
					{
						Security = security,
						OpenPrice = details[1].To<decimal>(),
						HighPrice = details[2].To<decimal>(),
						LowPrice = details[3].To<decimal>(),
						ClosePrice = details[4].To<decimal>(),
						TotalVolume = details[5].To<decimal>(),
						TimeFrame = (TimeSpan)timeFrame,
						// из терминала приходит время закрытия свечи
						OpenTime = details[0].To<DateTime>() - (TimeSpan)timeFrame,
						CloseTime = details[0].To<DateTime>(),
						State = CandleStates.Finished,
					};

					result.Add(candle);
				}
			}

			return result;
		}

		/// <summary>
		/// Получить временные диапазоны, для которых у данного источниках для передаваемой серии свечек есть данные.
		/// </summary>
		/// <param name="series">Серия свечек.</param>
		/// <returns>
		/// Временные диапазоны.
		/// </returns>
		public IEnumerable<Range<DateTime>> GetSupportedRanges(CandleSeries series)
		{
			if (series.CandleType == typeof(TimeFrameCandle) &&
				ReferenceEquals(series.Security.Trader, this) &&
				series.Arg is TimeSpan && AlfaTimeFrames.CanConvert((TimeSpan)series.Arg))
			{
				yield return new Range<DateTime>(DateTime.MinValue, series.GetMarketTime());
			}
		}

		/// <summary>
		/// Подписаться на получение исторических значения инструмента с заданной периодичностью.
		/// </summary>
		/// <param name="series">Серия свечек.</param><param name="from">Начальная дата, с которой необходимо получать данные.</param><param name="to">Конечная дата, до которой необходимо получать данные.</param>
		public void SubscribeHistoricalCandles(CandleSeries series, DateTime from, DateTime to)
		{
			if (series == null)
				throw new ArgumentNullException("series");

			var timeFrame = (TimeSpan)series.Arg;

			var timeInfo = new CandleTimeInfo
			{
				TimeStarted = from + timeFrame
			};

			ProcessHistoryCandles(series, timeFrame, new Range<DateTime>(from, to), true, ref timeInfo);
			_series[series] = timeInfo;
		}

		/// <summary>
		/// Остановить подписку получения исторических данных, ранее созданную через <see cref="M:StockSharp.Algo.Candles.IExternalCandleSource.SubscribeHistoricalCandles(StockSharp.Algo.Candles.CandleSeries,System.DateTime,System.DateTime)"/>.
		/// </summary>
		/// <param name="series">Серия свечек.</param>
		public void UnSubscribeHistoricalCandles(CandleSeries series)
		{
			_series.Remove(series);
		}

		/// <summary>
		/// Событие появления новых исторических свечек, полученных после подписки через <see cref="M:StockSharp.Algo.Candles.IExternalCandleSource.SubscribeHistoricalCandles(StockSharp.Algo.Candles.CandleSeries,System.DateTime,System.DateTime)"/>.
		/// </summary>
		public event Action<CandleSeries, IEnumerable<Candle>> NewHistoricalCandles;

		/// <summary>
		/// Получить исторические тайм-фрейм свечки из локальной базы AlfaDirect.
		/// </summary>
		/// <param name="series">Токен, для которого необходимо начать получать исторические свечки.</param>
		/// <param name="timeFrame">Тайм-фрейм.</param>
		/// <param name="range">Диапазон времени, для которого нужно получить свечки.</param>
		/// <param name="isHistory">True для исторических свечек, false для реал-тайм.</param>
		/// <param name="timeInfo"></param>
		/// <returns>Идентификатор получения исторических свечек.</returns>
		private void ProcessHistoryCandles(CandleSeries series, AlfaTimeFrames timeFrame, Range<DateTime> range, bool isHistory, ref CandleTimeInfo timeInfo)
		{
			//this.AddInfoLog("ProcessHistoryCandles: {0} - {1}, hist: {2}", range.Min, range.Max, isHistory);

			// TODO: this must be done in AD task
			var candles = (List<Candle>)GetLocalHistoryData(series.Security, timeFrame, range);

			//this.AddInfoLog("Received {0} candles", candles.Count);

			if (candles.Count == 0)
			{
				if (isHistory)
				{
					this.AddErrorLog("Ошибка загрузки исторических свечек. Проверьте их наличие в терминале!");
				}

				return;
			}

			var tfCandles = (from TimeFrameCandle tfCandle in candles select tfCandle).ToArray();

			if (isHistory)
			{
				foreach (var candle in tfCandles)
				{
					// Свеча незакончена, в процессе формирования, используем для CandleStarted.
					//					if (candle.OpenTime == range.Max)
					//					{
					//						CandlesStarted.SafeInvoke(token, new[] { candle });
					//
					//						timeInfo.TimeStarted = candle.OpenTime;
					//						timeInfo.IsStartedSent = true;
					//					}
					//					else
					//					{
					NewHistoricalCandles.SafeInvoke(series, new[] { candle });
					//					}
				}
			}
			else
			{
				var lastCandle = tfCandles.Last();

				foreach (var candle in tfCandles)
				{
					if (candle != lastCandle)
					{
						NewHistoricalCandles.SafeInvoke(series, new[] { candle });
						timeInfo.IsStartedSent = false;
					}
					else
					{
						if (!timeInfo.IsStartedSent)
						{
							NewHistoricalCandles.SafeInvoke(series, new[] { candle });
							timeInfo.TimeStarted = candle.CloseTime;
							timeInfo.IsStartedSent = true;

						}
						else
						{
							NewHistoricalCandles.SafeInvoke(series, new[] { candle });
						}
					}
				}
			}
		}
	}
}
